'use strict'

const DEFAULT_RAW = {
    after: '\n',
    beforeClose: '\n',
    beforeComment: '\n',
    beforeDecl: '\n',
    beforeOpen: ' ',
    beforeRule: '\n',
    colon: ': ',
    commentLeft: ' ',
    commentRight: ' ',
    emptyBody: '',
    indent: '    ',
    semicolon: false
}

function capitalize(str) {
    return str[0].toUpperCase() + str.slice(1)
}

class Stringifier {
    constructor(builder) {
        this.builder = builder
    }

    atrule(node, semicolon) {
        let name = '@' + node.name
        let params = node.params ? this.rawValue(node, 'params') : ''

        if (typeof node.raws.afterName !== 'undefined') {
            name += node.raws.afterName
        } else if (params) {
            name += ' '
        }

        if (node.nodes) {
            this.block(node, name + params)
        } else {
            let end = (node.raws.between || '') + (semicolon ? ';' : '')
            this.builder(name + params + end, node)
        }
    }

    beforeAfter(node, detect) {
        let value
        if (node.type === 'decl') {
            value = this.raw(node, null, 'beforeDecl')
        } else if (node.type === 'comment') {
            value = this.raw(node, null, 'beforeComment')
        } else if (detect === 'before') {
            value = this.raw(node, null, 'beforeRule')
        } else {
            value = this.raw(node, null, 'beforeClose')
        }

        let buf = node.parent
        let depth = 0
        while (buf && buf.type !== 'root') {
            depth += 1
            buf = buf.parent
        }

        if (value.includes('\n')) {
            let indent = this.raw(node, null, 'indent')
            if (indent.length) {
                for (let step = 0; step < depth; step++) {
                    value += indent
                }
            }
        }

        return value
    }

    block(node, start) {
        let between = this.raw(node, 'between', 'beforeOpen')
        this.builder(start + between + '{', node, 'start')

        let after
        if (node.nodes && node.nodes.length) {
            this.body(node)
            after = this.raw(node, 'after')
        } else {
            after = this.raw(node, 'after', 'emptyBody')
        }

        if (after) {
            this.builder(after)
        }
        this.builder('}', node, 'end')
    }

    body(node) {
        let last = node.nodes.length - 1
        while (last > 0) {
            if (node.nodes[last].type !== 'comment') {
                break
            }
            last -= 1
        }

        let semicolon = this.raw(node, 'semicolon')
        for (let i = 0; i < node.nodes.length; i++) {
            let child = node.nodes[i]
            let before = this.raw(child, 'before')
            if (before) {
                this.builder(before)
            }
            this.stringify(child, last !== i || semicolon)
        }
    }

    comment(node) {
        let left = this.raw(node, 'left', 'commentLeft')
        let right = this.raw(node, 'right', 'commentRight')
        this.builder('/*' + left + node.text + right + '*/', node)
    }

    decl(node, semicolon) {
        let between = this.raw(node, 'between', 'colon')
        let string = node.prop + between + this.rawValue(node, 'value')

        if (node.important) {
            string += node.raws.important || ' !important'
        }

        if (semicolon) {
            string += ';'
        }
        this.builder(string, node)
    }

    document(node) {
        this.body(node)
    }

    raw(node, own, detect) {
        let value
        if (!detect) {
            detect = own
        }

        // Already had
        if (own) {
            value = node.raws[own]
            if (typeof value !== 'undefined') {
                return value
            }
        }

        let parent = node.parent

        if (detect === 'before') {
            // Hack for first rule in CSS
            if (!parent || (parent.type === 'root' && parent.first === node)) {
                return ''
            }

            // `root` nodes in `document` should use only their own raws
            if (parent && parent.type === 'document') {
                return ''
            }
        }

        // Floating child without parent
        if (!parent) {
            return DEFAULT_RAW[detect]
        }

        // Detect style by other nodes
        let root = node.root()
        if (!root.rawCache) {
            root.rawCache = {}
        }
        if (typeof root.rawCache[detect] !== 'undefined') {
            return root.rawCache[detect]
        }

        if (detect === 'before' || detect === 'after') {
            return this.beforeAfter(node, detect)
        } else {
            let method = 'raw' + capitalize(detect)
            if (this[method]) {
                value = this[method](root, node)
            } else {
                root.walk(i => {
                    value = i.raws[own]
                    if (typeof value !== 'undefined') {
                        return false
                    }
                })
            }
        }

        if (typeof value === 'undefined') {
            value = DEFAULT_RAW[detect]
        }

        root.rawCache[detect] = value
        return value
    }

    rawBeforeClose(root) {
        let value
        root.walk(i => {
            if (i.nodes && i.nodes.length > 0) {
                if (typeof i.raws.after !== 'undefined') {
                    value = i.raws.after
                    if (value.includes('\n')) {
                        value = value.replace(/[^\n]+$/, '')
                    }
                    return false
                }
            }
        })
        if (value) {
            value = value.replace(/\S/g, '')
        }
        return value
    }

    rawBeforeComment(root, node) {
        let value
        root.walkComments(i => {
            if (typeof i.raws.before !== 'undefined') {
                value = i.raws.before
                if (value.includes('\n')) {
                    value = value.replace(/[^\n]+$/, '')
                }
                return false
            }
        })
        if (typeof value === 'undefined') {
            value = this.raw(node, null, 'beforeDecl')
        } else if (value) {
            value = value.replace(/\S/g, '')
        }
        return value
    }

    rawBeforeDecl(root, node) {
        let value
        root.walkDecls(i => {
            if (typeof i.raws.before !== 'undefined') {
                value = i.raws.before
                if (value.includes('\n')) {
                    value = value.replace(/[^\n]+$/, '')
                }
                return false
            }
        })
        if (typeof value === 'undefined') {
            value = this.raw(node, null, 'beforeRule')
        } else if (value) {
            value = value.replace(/\S/g, '')
        }
        return value
    }

    rawBeforeOpen(root) {
        let value
        root.walk(i => {
            if (i.type !== 'decl') {
                value = i.raws.between
                if (typeof value !== 'undefined') {
                    return false
                }
            }
        })
        return value
    }

    rawBeforeRule(root) {
        let value
        root.walk(i => {
            if (i.nodes && (i.parent !== root || root.first !== i)) {
                if (typeof i.raws.before !== 'undefined') {
                    value = i.raws.before
                    if (value.includes('\n')) {
                        value = value.replace(/[^\n]+$/, '')
                    }
                    return false
                }
            }
        })
        if (value) {
            value = value.replace(/\S/g, '')
        }
        return value
    }

    rawColon(root) {
        let value
        root.walkDecls(i => {
            if (typeof i.raws.between !== 'undefined') {
                value = i.raws.between.replace(/[^\s:]/g, '')
                return false
            }
        })
        return value
    }

    rawEmptyBody(root) {
        let value
        root.walk(i => {
            if (i.nodes && i.nodes.length === 0) {
                value = i.raws.after
                if (typeof value !== 'undefined') {
                    return false
                }
            }
        })
        return value
    }

    rawIndent(root) {
        if (root.raws.indent) {
            return root.raws.indent
        }
        let value
        root.walk(i => {
            let p = i.parent
            if (p && p !== root && p.parent && p.parent === root) {
                if (typeof i.raws.before !== 'undefined') {
                    let parts = i.raws.before.split('\n')
                    value = parts[parts.length - 1]
                    value = value.replace(/\S/g, '')
                    return false
                }
            }
        })
        return value
    }

    rawSemicolon(root) {
        let value
        root.walk(i => {
            if (i.nodes && i.nodes.length && i.last.type === 'decl') {
                value = i.raws.semicolon
                if (typeof value !== 'undefined') {
                    return false
                }
            }
        })
        return value
    }

    rawValue(node, prop) {
        let value = node[prop]
        let raw = node.raws[prop]
        if (raw && raw.value === value) {
            return raw.raw
        }

        return value
    }

    root(node) {
        this.body(node)
        if (node.raws.after) {
            this.builder(node.raws.after)
        }
    }

    rule(node) {
        this.block(node, this.rawValue(node, 'selector'))
        if (node.raws.ownSemicolon) {
            this.builder(node.raws.ownSemicolon, node, 'end')
        }
    }

    stringify(node, semicolon) {
        /* c8 ignore start */
        if (!this[node.type]) {
            throw new Error(
                'Unknown AST node type ' +
                node.type +
                    '. ' +
                    'Maybe you need to change PostCSS stringifier.'
            )
        }
        /* c8 ignore stop */
        this[node.type](node, semicolon)
    }
}

export { Stringifier }

export default Stringifier
